from __future__ import division
import sys
import os
import operator
import glob
from struct import calcsize, unpack, unpack_from

import numpy
import pylab

DEBUG = False
if __name__ == '__main__':
    DEBUG = True

# BioLogic LP System Properties
UV_FULLSCALE = 5.0
UV_PATHLEN = 0.2 # centimeters
COND_FULLSCALE = 500.0 # mS/cm
# line volume between the detectors and fraction collector
UV_OFFSET = 0 # mL
COND_OFFSET = 0 # mL

# File Format constants
NOTES_LEN_ADDR = 0x88
NOTES_LEN_FMT = 'h'
METHOD_LEN_FMT = 'h'

FRAC_ADDR = 0x01F5
FRAC_LABEL_LEN_FMT = 'bx'
FRAC_HEIGHT_FMT = 'Hx'
FRAC_COLOR_FMT = 'bx'
FRAC_ENDP_FMT = 'f'

DATA_ADDR = 0x04A6 # (1190) !!! INVALID GENERALIZATION
DATA_PACK_FMT = 'ff'
DATA_POINTS_PER_MIN = 300

class CHD_Data(object):

    def __init__(self, fdat):
        self.parse(fdat)
    
    def parse(self, fdat):
        # HEADER DATA
        # fields marked with "_" are unknown.  formats guessed
        (
            # first 4 bytes always seem to be 1.1 (floating, obviously)
            unk_00, graph_t_min, graph_t_max, graph_au_min,
            
            # last in this line is always 1 (integer)
            graph_au_max, graph_cond_min, graph_cond_max, unk_1C,
            
            unk_20, unk_24, unk_28, data_len,   # 26, 32, 52, 45003
            
            unk_30                       # 60002
        ) = unpack_from('ffff fffi iiii i', fdat)
        
        self.t_range = (graph_t_min, graph_t_max)
        self.au_range = (graph_au_min, graph_au_max)
        self.cond_range = (graph_cond_min, graph_cond_max)
        
        # NOTES AND METHOD DESCRIPTION
        # ----------------------------
        # quick check, are they all zero between here and there?  we missing fields?
        #assert not sum([x != '\0' for x in fdat[0x34:NOTES_LEN_ADDR]])
        if sum([x != '\0' for x in fdat[0x34:NOTES_LEN_ADDR]]):
            print "non-zero value found!"
            print fdat[0x34:NOTES_LEN_ADDR]
            sys.exit()

        i = NOTES_LEN_ADDR
        
        notes_len = unpack_from(NOTES_LEN_FMT, fdat, i)[0]
        i = i + calcsize(NOTES_LEN_FMT)
        
        self.notes = ''
        if notes_len:
            self.notes = fdat[i:i + notes_len]
            i = i + notes_len

        method_len = unpack_from(METHOD_LEN_FMT, fdat, i)[0]
        i = i + calcsize(METHOD_LEN_FMT)
        
        self.method = ''
        if method_len:
            self.method = fdat[i:i + method_len]
            i = i + method_len

        # TOTAL TIME?
        # 32-bit float immediately following method string is equal to the 
        # total length of time in minutes; where the "E" marker is
        self.total_time = unpack_from('f', fdat, i)[0]
        i = i + 4

        # ----- 156 (0x9C) ZEROES -----
        SPACE_METHOD_FRAC = 156
        assert not sum([x != '\0' for x in fdat[i:i+SPACE_METHOD_FRAC]])
        i = i + SPACE_METHOD_FRAC
        # -----------------------------
                
        # FRACTION MARKERS
        # len(tag)  tag          height? color?     float(frac-start)
        # 01 00  -  53 00     -  FF FF 00 32 00  -  00 00 00 00
        # [...]
        # 01 00  -  31 00     -  FF 00 00 0A 00  -  3A 6D A0 40
        # 02 00  -  31 30 00  -  FF 00 00 0A 00  -  3D 0A 48 42
        # [...] 
        # 01 00  -  45 00     -  FF FF 00 28 00  -  B5 01 16 43
        self.fractions = []
        while True:
            #di = calcsize(FRAC_LABEL_LEN_FMT)
            #label_len = unpack(FRAC_LABEL_LEN_FMT, fdat[i:i+di])[0]
            #i = i + di
            label_len = unpack_from(FRAC_LABEL_LEN_FMT, fdat, i)[0]
            i = i + calcsize(FRAC_LABEL_LEN_FMT)
            
            # '=' signals to ignore 32-bit alignments in the data structure
            fmt = '={0}sx{1}{2}{3}'.format(
                    label_len, 
                    FRAC_HEIGHT_FMT, 
                    FRAC_COLOR_FMT, 
                    FRAC_ENDP_FMT)
            self.fractions.append(unpack_from(fmt, fdat, i))
            i = i + calcsize(fmt)
                        
            # probably only one is actually useful
            if self.fractions[-1][2] == 0x28: break
            #if self.fractions[-1][3] == totaltime: break  # No!  Can quit prematurely.
            
        # ----- ??? ZEROES -----
        # OXA1S67G 062111.chd only has 12 :(  It starts the data at 0x03B7
        # oxa1-2011-06-23-pump-loaded.chd has 264, starts data at 0x05AD
        # w/ note padding it's still 264 long, data starting at 0x5ED
        # 072710_n152g.chd is 72 long, data starting at 0x0474
        
        # The number of zeroes seem to be in multiples of 4; one of them appears
        # to be off (ZEROES mod 4 = 2), but this is due to the first float
        # containing zeroes (5.0 = '00 00 A0 40')
        
        # the stupid thing isn't divisible by 8; the length of the value pair (2x
        # 4-byte floats
        endfrac = i
        # ----------------------
        # find start...this is fragile as it will break if the first absorbance
        # reading is exactly 0.
        while True:
            if unpack_from('f', fdat, i)[0] == 0:
                i = i + 4
            else:
                break
        # ----------------------
        bgndata = i
        
        # ABORBANCE/CONDUCTIVITY DATA
        # interleaved 32-bit floats starting at 0x04A8, length (in pairs of values) 
        # at 0x04A6 (so 8x that in bytes)
        #i = DATA_ADDR
        #print "Data points (AU/Cond. pairs): %i" % data_len

        #datar = fdat[i:i + data_len * calcsize(DATA_PACK_FMT)]
        #data = unpack('ff' * data_len, datar)
        data = unpack_from('ff' * data_len, fdat, i)
        
        self.time = numpy.arange(0, data_len)/DATA_POINTS_PER_MIN
        volume = None
        self.au = data[::2]
        self.cond = data[1::2]
        #self.au = numpy.column_stack((time, data[::2]))
        #self.cond = numpy.column_stack((time, data[1::2]))
        #self.au = numpy.column_stack((time[au_box_tails:-au_box_tails], numpy.convolve(data[::2],au_filter,'valid')))
        #self.cond = numpy.column_stack((time[COND_RCF_LEN:-COND_RCF_LEN], numpy.convolve(data[1::2],cond_filter,'valid')))

        # sanity check
        '''
        for val in zip(*self.au)[1]:
            #assert (-UV_FULLSCALE <= val < -5e-10) or (val == 0) or (5e-10 < val <= UV_FULLSCALE)
            if not ((-UV_FULLSCALE <= val < -5e-10) or (val == 0) or (5e-10 < val <= UV_FULLSCALE)):
                print "failed with val: {0}".format(val)
                assert False
        for val in zip(*self.cond)[1]   :
            assert 0 <= val <= COND_FULLSCALE
        '''
        
        i = i + data_len * calcsize(DATA_PACK_FMT)

        if DEBUG:
            # some number of zeroes, not always...
            # ----- 120000 (0x0001:D4C0) ZEROES ----- (as in our first test file)
            # 0x30 in the file is 60002, eerily close to a multiple of it.
            trailer = len(fdat) - i
       
            print ('%f,'  '%i,'   '%i,'   '%i,'   '%i,'   '%i,'   '%i,'     '%i,'    '%i,'    '%i,'   % 
                  (unk_00, unk_1C, unk_20, unk_24, unk_28, unk_30, data_len, endfrac, bgndata, trailer))
                  
def main(argv=None):
    if argv is None:
        argv = sys.argv
    print "#file#, unk_00, unk_1C, unk_20, unk_24, unk_28, unk_30, endfrac, bgndata, trailer"
    
    dataz = []
    #for fn in glob.glob('./test-files/test*.chd'):
    for fn in glob.glob('./test-files/*.chd'):
        #print '{0},'.format(fn),
        with open(fn, 'rb') as f:
            fdat = f.read()
        dataz.append(CHD_Data(fdat))
    
    #print dataz[1].au[0:100]
        
if __name__ == '__main__':
    sys.exit(main())